/*
 * Copyright 2006-2009, Stephan Aßmus <superstippi@gmx.de>.
 * Copyright 2023, Haiku, Inc.
 * All rights reserved. Distributed under the terms of the MIT License.
 *
 * Authors:
 *             Zardshard
 */

#include "PathManipulator.h"

#include <algorithm>
#include <float.h>
#include <stdio.h>
#include <vector>

#include <Catalog.h>
#include <Cursor.h>
#include <Locale.h>
#include <Message.h>
#include <MenuItem.h>
#include <PopUpMenu.h>
#include <StackOrHeapArray.h>
#include <Window.h>

#include "cursors.h"
#include "support.h"

#include "CanvasView.h"

#include "AddPointCommand.h"
#include "ChangePointCommand.h"
//#include "CloseCommand.h"
#include "InsertPointCommand.h"
#include "FlipPointsCommand.h"
//#include "NewPathCommand.h"
#include "NudgePointsCommand.h"
//#include "RemovePathCommand.h"
#include "RemovePointsCommand.h"
//#include "ReversePathCommand.h"
//#include "SelectPathCommand.h"
//#include "SelectPointsCommand.h"
#include "SplitPointsCommand.h"
#include "TransformPointsBox.h"


#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "Icon-O-Matic-PathManipulator"
#define POINT_EXTEND 3.0
#define CONTROL_POINT_EXTEND 2.0
#define INSERT_DIST_THRESHOLD 7.0
#define MOVE_THRESHOLD 9.0


enum {
	UNDEFINED,

	NEW_PATH,

	ADD_POINT,
	INSERT_POINT,
	MOVE_POINT,
	MOVE_POINT_IN,
	MOVE_POINT_OUT,
	CLOSE_PATH,

	TOGGLE_SHARP,
	TOGGLE_SHARP_IN,
	TOGGLE_SHARP_OUT,

	REMOVE_POINT,
	REMOVE_POINT_IN,
	REMOVE_POINT_OUT,

	SELECT_POINTS,
	TRANSFORM_POINTS,
	TRANSLATE_POINTS,

	SELECT_SUB_PATH,
};

enum {
	MSG_TRANSFORM				= 'strn',
	MSG_REMOVE_POINTS			= 'srmp',
	MSG_UPDATE_SHAPE_UI			= 'udsi',

	MSG_SPLIT_POINTS			= 'splt',
	MSG_FLIP_POINTS				= 'flip',
};

inline const char*
string_for_mode(uint32 mode)
{
	switch (mode) {
		case UNDEFINED:
			return "UNDEFINED";
		case NEW_PATH:
			return "NEW_PATH";
		case ADD_POINT:
			return "ADD_POINT";
		case INSERT_POINT:
			return "INSERT_POINT";
		case MOVE_POINT:
			return "MOVE_POINT";
		case MOVE_POINT_IN:
			return "MOVE_POINT_IN";
		case MOVE_POINT_OUT:
			return "MOVE_POINT_OUT";
		case CLOSE_PATH:
			return "CLOSE_PATH";
		case TOGGLE_SHARP:
			return "TOGGLE_SHARP";
		case TOGGLE_SHARP_IN:
			return "TOGGLE_SHARP_IN";
		case TOGGLE_SHARP_OUT:
			return "TOGGLE_SHARP_OUT";
		case REMOVE_POINT:
			return "REMOVE_POINT";
		case REMOVE_POINT_IN:
			return "REMOVE_POINT_IN";
		case REMOVE_POINT_OUT:
			return "REMOVE_POINT_OUT";
		case SELECT_POINTS:
			return "SELECT_POINTS";
		case TRANSFORM_POINTS:
			return "TRANSFORM_POINTS";
		case TRANSLATE_POINTS:
			return "TRANSLATE_POINTS";
		case SELECT_SUB_PATH:
			return "SELECT_SUB_PATH";
	}
	return "<unknown mode>";
}

// NOTE: this class extends std::vector<int32> since neither BList or
// BObjectList would suffice. The backing array of BList and BObjectList is a
// void* array. The Items function should return an int32 array. This is a
// problem since sizeof(void*) is not necessarily equal to sizeof(int32).
class PathManipulator::Selection : protected std::vector<int32>
{
public:
	inline Selection(int32 count = 20)
		: _inherited() { reserve(count); }
	inline ~Selection() {}

	inline void Add(int32 value)
	{
		if (value >= 0) {
			// keep the list sorted
			insert(std::upper_bound(begin(), end(), value), value);
		}
	}

	inline bool Remove(int32 value)
	{
		if (!Contains(value))
			return false;
		erase(std::lower_bound(begin(), end(), value));
		return true;
	}

	inline bool Contains(int32 value) const
		{ return std::binary_search(begin(), end(), value); }

	inline bool IsEmpty() const
		{ return size() == 0; }

	inline int32 IndexAt(int32 index) const
		{ return at(index); }

	inline void MakeEmpty()
		{ clear(); }

	inline const int32* Items() const
		{ return &(*this)[0]; }

	inline const int32 CountItems() const
		{ return size(); }

	inline Selection& operator =(const Selection& other)
	{
		_inherited::operator=(other);
		return *this;
	}

	inline bool operator ==(const Selection& other)
		{ return (_inherited)*this == (_inherited)other; }

	inline bool operator !=(const Selection& other)
		{ return (_inherited)*this != (_inherited)other; }

private:
	typedef std::vector<int32> _inherited;
};


PathManipulator::PathManipulator(VectorPath* path)
	: Manipulator(NULL),
	  fCanvasView(NULL),

	  fCommandDown(false),
	  fOptionDown(false),
	  fShiftDown(false),
	  fAltDown(false),

	  fClickToClose(false),

	  fMode(NEW_PATH),
	  fFallBackMode(SELECT_POINTS),

	  fMouseDown(false),

	  fPath(path),
	  fCurrentPathPoint(-1),

	  fChangePointCommand(NULL),
	  fInsertPointCommand(NULL),
	  fAddPointCommand(NULL),

	  fSelection(new Selection()),
	  fOldSelection(new Selection()),
	  fTransformBox(NULL),

	  fNudgeOffset(0.0, 0.0),
	  fLastNudgeTime(system_time()),
	  fNudgeCommand(NULL)
{
	fPath->AcquireReference();
	fPath->AddListener(this);
	fPath->AddObserver(this);
}


PathManipulator::~PathManipulator()
{
	delete fChangePointCommand;
	delete fInsertPointCommand;
	delete fAddPointCommand;

	delete fSelection;
	delete fOldSelection;
	delete fTransformBox;

	delete fNudgeCommand;

	fPath->RemoveObserver(this);
	fPath->RemoveListener(this);
	fPath->ReleaseReference();
}


// #pragma mark -


class StrokePathIterator : public VectorPath::Iterator {
 public:
					StrokePathIterator(CanvasView* canvasView,
									   BView* drawingView)
						: fCanvasView(canvasView),
						  fDrawingView(drawingView)
					{
						fDrawingView->SetHighColor(0, 0, 0, 255);
						fDrawingView->SetDrawingMode(B_OP_OVER);
					}
	virtual			~StrokePathIterator()
					{}

	virtual	void	MoveTo(BPoint point)
					{
						fBlack = true;
						fSkip = false;
						fDrawingView->SetHighColor(0, 0, 0, 255);

						fCanvasView->ConvertFromCanvas(&point);
						fDrawingView->MovePenTo(point);
					}
	virtual	void	LineTo(BPoint point)
					{
						fCanvasView->ConvertFromCanvas(&point);
						if (!fSkip) {
							if (fBlack)
								fDrawingView->SetHighColor(255, 255, 255, 255);
							else
								fDrawingView->SetHighColor(0, 0, 0, 255);
							fBlack = !fBlack;

							fDrawingView->StrokeLine(point);
						} else {
							fDrawingView->MovePenTo(point);
						}
						fSkip = !fSkip;
					}

 private:
	CanvasView*		fCanvasView;
	BView*			fDrawingView;
	bool			fBlack;
	bool			fSkip;
};


void
PathManipulator::Draw(BView* into, BRect updateRect)
{
	// draw the Bezier curve, but only if not "editing",
	// the path is actually on top all other modifiers
	// TODO: make this customizable in the GUI

	#if __HAIKU__
	uint32 flags = into->Flags();
	into->SetFlags(flags | B_SUBPIXEL_PRECISE);
	#endif // __HAIKU__

	StrokePathIterator iterator(fCanvasView, into);
	fPath->Iterate(&iterator, fCanvasView->ZoomLevel());

	#if __HAIKU__
	into->SetFlags(flags);
	#endif // __HAIKU__

	into->SetLowColor(0, 0, 0, 255);
	BPoint point;
	BPoint pointIn;
	BPoint pointOut;
	rgb_color focusColor = (rgb_color){ 255, 0, 0, 255 };
	rgb_color highlightColor = (rgb_color){ 60, 60, 255, 255 };
	for (int32 i = 0; fPath->GetPointsAt(i, point, pointIn, pointOut); i++) {
		bool highlight = fCurrentPathPoint == i;
		bool selected = fSelection->Contains(i);
		rgb_color normal = selected ? focusColor : (rgb_color){ 0, 0, 0, 255 };
		into->SetLowColor(normal);
		into->SetHighColor(255, 255, 255, 255);
		// convert to view coordinate space
		fCanvasView->ConvertFromCanvas(&point);
		fCanvasView->ConvertFromCanvas(&pointIn);
		fCanvasView->ConvertFromCanvas(&pointOut);
		// connect the points belonging to one control point
		into->SetDrawingMode(B_OP_INVERT);
		into->StrokeLine(point, pointIn);
		into->StrokeLine(point, pointOut);
		// draw main control point
		if (highlight && (fMode == MOVE_POINT ||
						  fMode == TOGGLE_SHARP ||
						  fMode == REMOVE_POINT ||
						  fMode == SELECT_POINTS ||
						  fMode == CLOSE_PATH)) {

			into->SetLowColor(highlightColor);
		}

		into->SetDrawingMode(B_OP_COPY);
		BRect r(point, point);
		r.InsetBy(-POINT_EXTEND, -POINT_EXTEND);
		into->StrokeRect(r, B_SOLID_LOW);
		r.InsetBy(1.0, 1.0);
		into->FillRect(r, B_SOLID_HIGH);
		// draw in control point
		if (highlight && (fMode == MOVE_POINT_IN ||
						  fMode == TOGGLE_SHARP_IN ||
						  fMode == REMOVE_POINT_IN ||
						  fMode == SELECT_POINTS))
			into->SetLowColor(highlightColor);
		else
			into->SetLowColor(normal);
		if (selected) {
			into->SetHighColor(220, 220, 220, 255);
		} else {
			into->SetHighColor(170, 170, 170, 255);
		}
		if (pointIn != point) {
			r.Set(pointIn.x - CONTROL_POINT_EXTEND, pointIn.y - CONTROL_POINT_EXTEND,
				  pointIn.x + CONTROL_POINT_EXTEND, pointIn.y + CONTROL_POINT_EXTEND);
			into->StrokeRect(r, B_SOLID_LOW);
			r.InsetBy(1.0, 1.0);
			into->FillRect(r, B_SOLID_HIGH);
		}
		// draw out control point
		if (highlight && (fMode == MOVE_POINT_OUT ||
						  fMode == TOGGLE_SHARP_OUT ||
						  fMode == REMOVE_POINT_OUT ||
						  fMode == SELECT_POINTS))
			into->SetLowColor(highlightColor);
		else
			into->SetLowColor(normal);
		if (pointOut != point) {
			r.Set(pointOut.x - CONTROL_POINT_EXTEND, pointOut.y - CONTROL_POINT_EXTEND,
				  pointOut.x + CONTROL_POINT_EXTEND, pointOut.y + CONTROL_POINT_EXTEND);
			into->StrokeRect(r, B_SOLID_LOW);
			r.InsetBy(1.0, 1.0);
			into->FillRect(r, B_SOLID_HIGH);
		}
	}

	if (fTransformBox) {
		fTransformBox->Draw(into, updateRect);
	}
}


// #pragma mark -


bool
PathManipulator::MouseDown(BPoint where)
{
	fMouseDown = true;

	if (fMode == TRANSFORM_POINTS) {
		if (fTransformBox) {
			fTransformBox->MouseDown(where);

//			if (!fTransformBox->IsRotating())
//				fCanvasView->SetAutoScrolling(true);
		}
		return true;
	}

	if (fMode == MOVE_POINT &&
		fSelection->CountItems() > 1 &&
		fSelection->Contains(fCurrentPathPoint)) {
		fMode = TRANSLATE_POINTS;
	}

	// apply the canvas view mouse filter depending on current mode
	if (fMode == ADD_POINT || fMode == TRANSLATE_POINTS)
		fCanvasView->FilterMouse(&where);

	BPoint canvasWhere = where;
	fCanvasView->ConvertToCanvas(&canvasWhere);

	// maybe we're changing some point, so we construct the
	// "ChangePointCommand" here so that the point is remembered
	// in its current state
	// apply the canvas view mouse filter depending on current mode
	delete fChangePointCommand;
	fChangePointCommand = NULL;
	switch (fMode) {
		case TOGGLE_SHARP:
		case TOGGLE_SHARP_IN:
		case TOGGLE_SHARP_OUT:
		case MOVE_POINT:
		case MOVE_POINT_IN:
		case MOVE_POINT_OUT:
		case REMOVE_POINT_IN:
		case REMOVE_POINT_OUT:
			fChangePointCommand = new ChangePointCommand(fPath,
														 fCurrentPathPoint,
														 fSelection->Items(),
														 fSelection->CountItems());
			_Select(fCurrentPathPoint, fShiftDown);
			break;
	}

	// at this point we init doing something
	switch (fMode) {
		case ADD_POINT:
			_AddPoint(canvasWhere);
			break;
		case INSERT_POINT:
			_InsertPoint(canvasWhere, fCurrentPathPoint);
			break;

		case TOGGLE_SHARP:
			_SetSharp(fCurrentPathPoint);
			// continue by dragging out the _connected_ in/out points
			break;
		case TOGGLE_SHARP_IN:
			_SetInOutConnected(fCurrentPathPoint, false);
			// continue by moving the "in" point
			_SetMode(MOVE_POINT_IN);
			break;
		case TOGGLE_SHARP_OUT:
			_SetInOutConnected(fCurrentPathPoint, false);
			// continue by moving the "out" point
			_SetMode(MOVE_POINT_OUT);
			break;

		case MOVE_POINT:
		case MOVE_POINT_IN:
		case MOVE_POINT_OUT:
			// the right thing happens since "fCurrentPathPoint"
			// points to the correct index
			break;

		case CLOSE_PATH:
//			SetClosed(true, true);
			break;

		case REMOVE_POINT:
			if (fPath->CountPoints() == 1) {
//				fCanvasView->Perform(new RemovePathCommand(this, fPath));
			} else {
				fCanvasView->Perform(new RemovePointsCommand(fPath,
															 fCurrentPathPoint,
															 fSelection->Items(),
															 fSelection->CountItems()));
				_RemovePoint(fCurrentPathPoint);
			}
			break;
		case REMOVE_POINT_IN:
			_RemovePointIn(fCurrentPathPoint);
			break;
		case REMOVE_POINT_OUT:
			_RemovePointOut(fCurrentPathPoint);
			break;

		case SELECT_POINTS: {
			// TODO: this works so that you can deselect all points
			// when clicking outside the path even if pressing shift
			// in case the path is open... a better way would be
			// to deselect all on mouse up, if the mouse has not moved
			bool appendSelection;
			if (fPath->IsClosed())
				appendSelection = fShiftDown;
			else
				appendSelection = fShiftDown && fCurrentPathPoint >= 0;

			if (!appendSelection) {
				fSelection->MakeEmpty();
				_UpdateSelection();
			}
			*fOldSelection = *fSelection;
			if (fCurrentPathPoint >= 0) {
				_Select(fCurrentPathPoint, appendSelection);
			}
			fCanvasView->BeginRectTracking(BRect(where, where),
				B_TRACK_RECT_CORNER);
			break;
		}
	}

	fTrackingStart = canvasWhere;
	// remember the subpixel position
	// so that MouseMoved() will work even before
	// the integer position becomes different
	fLastCanvasPos = where;
	fCanvasView->ConvertToCanvas(&fLastCanvasPos);

	// the reason to exclude the select mode
	// is that the BView rect tracking does not
	// scroll the rect starting point along with us
	// (since we're doing no real scrolling)
//	if (fMode != SELECT_POINTS)
//		fCanvasView->SetAutoScrolling(true);

	UpdateCursor();

	return true;
}


void
PathManipulator::MouseMoved(BPoint where)
{
	fCanvasView->FilterMouse(&where);
		// NOTE: only filter mouse coords in mouse moved, no other
		// mouse function
	BPoint canvasWhere = where;
	fCanvasView->ConvertToCanvas(&canvasWhere);

	// since the tablet is generating mouse moved messages
	// even if only the pressure changes (and not the actual mouse position)
	// we insert this additional check to prevent too much calculation
	if (fLastCanvasPos == canvasWhere)
		return;

	fLastCanvasPos = canvasWhere;

	if (fMode == TRANSFORM_POINTS) {
		if (fTransformBox) {
			fTransformBox->MouseMoved(where);
		}
		return;
	}

	if (fMode == CLOSE_PATH) {
		// continue by moving the point
		_SetMode(MOVE_POINT);
		delete fChangePointCommand;
		fChangePointCommand = new ChangePointCommand(fPath,
													 fCurrentPathPoint,
													 fSelection->Items(),
													 fSelection->CountItems());
	}

//	if (!fPrecise) {
//		float offset = fmod(fOutlineWidth, 2.0) / 2.0;
//		canvasWhere.point += BPoint(offset, offset);
//	}

	switch (fMode) {
		case ADD_POINT:
		case INSERT_POINT:
		case TOGGLE_SHARP:
			// drag the "out" control point, mirror the "in" control point
			fPath->SetPointOut(fCurrentPathPoint, canvasWhere, true);
			break;
		case MOVE_POINT:
			// drag all three control points at once
			fPath->SetPoint(fCurrentPathPoint, canvasWhere);
			break;
		case MOVE_POINT_IN:
			// drag in control point
			fPath->SetPointIn(fCurrentPathPoint, canvasWhere);
			break;
		case MOVE_POINT_OUT:
			// drag out control point
			fPath->SetPointOut(fCurrentPathPoint, canvasWhere);
			break;

		case SELECT_POINTS: {
			// change the selection
			BRect r;
			r.left = min_c(fTrackingStart.x, canvasWhere.x);
			r.top = min_c(fTrackingStart.y, canvasWhere.y);
			r.right = max_c(fTrackingStart.x, canvasWhere.x);
			r.bottom = max_c(fTrackingStart.y, canvasWhere.y);
			_Select(r);
			break;
		}

		case TRANSLATE_POINTS: {
			BPoint offset = canvasWhere - fTrackingStart;
			_Nudge(offset);
			fTrackingStart = canvasWhere;
			break;
		}
	}
}


Command*
PathManipulator::MouseUp()
{
	// prevent carrying out actions more than once by only
	// doing it if "fMouseDown" is true at the point of
	// entering this function
	if (!fMouseDown)
		return NULL;
	fMouseDown = false;

	if (fMode == TRANSFORM_POINTS) {
		if (fTransformBox) {
			return fTransformBox->MouseUp();
		}
		return NULL;
	}

	Command* command = NULL;

	switch (fMode) {

		case ADD_POINT:
			command = fAddPointCommand;
			fAddPointCommand = NULL;
			_SetMode(MOVE_POINT_OUT);
			break;

		case INSERT_POINT:
			command = fInsertPointCommand;
			fInsertPointCommand = NULL;
			break;

		case SELECT_POINTS:
			if (*fSelection != *fOldSelection) {
//				command = new SelectPointsCommand(this, fPath,
//												  fOldSelection->Items(),
//												  fOldSelection->CountItems(),
//												  fSelection->Items(),
//												  fSelection->CountItems()));
			}
			fCanvasView->EndRectTracking();
			break;

		case TOGGLE_SHARP:
		case TOGGLE_SHARP_IN:
		case TOGGLE_SHARP_OUT:
		case MOVE_POINT:
		case MOVE_POINT_IN:
		case MOVE_POINT_OUT:
		case REMOVE_POINT_IN:
		case REMOVE_POINT_OUT:
			command = fChangePointCommand;
			fChangePointCommand = NULL;
			break;

		case TRANSLATE_POINTS:
			if (!fNudgeCommand) {
				// select just the point that was clicked
				*fOldSelection = *fSelection;
				if (fCurrentPathPoint >= 0) {
					_Select(fCurrentPathPoint, fShiftDown);
				}
				if (*fSelection != *fOldSelection) {
//					command = new SelectPointsCommand(this, fPath,
//													  fOldSelection->Items(),
//													  fOldSelection->CountItems(),
//													  fSelection->Items(),
//													  fSelection->CountItems()));
				}
			} else {
				command = _FinishNudging();
			}
			break;
	}

	return command;
}


bool
PathManipulator::MouseOver(BPoint where)
{
	if (fMode == TRANSFORM_POINTS) {
		if (fTransformBox) {
			return fTransformBox->MouseOver(where);
		}
		return false;
	}

	BPoint canvasWhere = where;
	fCanvasView->ConvertToCanvas(&canvasWhere);

	// since the tablet is generating mouse moved messages
	// even if only the pressure changes (and not the actual mouse position)
	// we insert this additional check to prevent too much calculation
	if (fMouseDown && fLastCanvasPos == canvasWhere)
		return false;

	fLastCanvasPos = canvasWhere;

	// hit testing
	// (use a subpixel mouse pos)
	fCanvasView->ConvertToCanvas(&where);
	_SetModeForMousePos(where);

	// TODO: always true?
	return true;
}


bool
PathManipulator::DoubleClicked(BPoint where)
{
	return false;
}


bool
PathManipulator::ShowContextMenu(BPoint where)
{
	// Change the selection to the current point if it isn't currently
	// selected. This could will only be chosen if the user right-clicked
	// a path point directly. 
	if (fCurrentPathPoint >= 0 && !fSelection->Contains(fCurrentPathPoint)) {
		fSelection->MakeEmpty();
		_UpdateSelection();
		*fOldSelection = *fSelection;
		_Select(fCurrentPathPoint, false);
	}

	BPopUpMenu* menu = new BPopUpMenu("context menu", false, false);
	BMessage* message;
	BMenuItem* item;

	bool hasSelection = fSelection->CountItems() > 0;

	if (fCurrentPathPoint < 0) {
		message = new BMessage(B_SELECT_ALL);
		item = new BMenuItem(B_TRANSLATE("Select all"), message, 'A');
		menu->AddItem(item);

		menu->AddSeparatorItem();
	}

	message = new BMessage(MSG_TRANSFORM);
	item = new BMenuItem(B_TRANSLATE("Transform"), message, 'T');
	item->SetEnabled(hasSelection);
	menu->AddItem(item);

	message = new BMessage(MSG_SPLIT_POINTS);
	item = new BMenuItem(B_TRANSLATE("Split"), message);
	item->SetEnabled(hasSelection);
	menu->AddItem(item);

	message = new BMessage(MSG_FLIP_POINTS);
	item = new BMenuItem(B_TRANSLATE("Flip"), message);
	item->SetEnabled(hasSelection);
	menu->AddItem(item);

	message = new BMessage(MSG_REMOVE_POINTS);
	item = new BMenuItem(B_TRANSLATE("Remove"), message);
	item->SetEnabled(hasSelection);
	menu->AddItem(item);

	// go
	menu->SetTargetForItems(fCanvasView);
	menu->SetAsyncAutoDestruct(true);
	menu->SetFont(be_plain_font);
	where = fCanvasView->ConvertToScreen(where);
	BRect mouseRect(where, where);
	mouseRect.InsetBy(-10.0, -10.0);
	where += BPoint(5.0, 5.0);
	menu->Go(where, true, false, mouseRect, true);

	return true;
}


// #pragma mark -


BRect
PathManipulator::Bounds()
{
	BRect r = _ControlPointRect();
	fCanvasView->ConvertFromCanvas(&r);
	return r;
}


BRect
PathManipulator::TrackingBounds(BView* withinView)
{
	return withinView->Bounds();
}


// #pragma mark -


bool
PathManipulator::MessageReceived(BMessage* message, Command** _command)
{
	bool result = true;
	switch (message->what) {
		case MSG_TRANSFORM:
			if (!fSelection->IsEmpty())
				_SetMode(TRANSFORM_POINTS);
			break;
		case MSG_REMOVE_POINTS:
			*_command = _Delete();
			break;
		case MSG_SPLIT_POINTS:
			*_command = new SplitPointsCommand(fPath,
											   fSelection->Items(),
											   fSelection->CountItems());
			break;
		case MSG_FLIP_POINTS:
			*_command = new FlipPointsCommand(fPath,
											  fSelection->Items(),
											  fSelection->CountItems());
			break;
		case B_SELECT_ALL: {
			int32 count = fPath->CountPoints();
			int32 indices[count];

			for (int32 i = 0; i < count; i++)
				indices[i] = i;

			_Select(indices, count);
			break;
		}
		default:
			result = false;
			break;
	}
	return result;
}


void
PathManipulator::ModifiersChanged(uint32 modifiers)
{
	fCommandDown = modifiers & B_COMMAND_KEY;
	fOptionDown = modifiers & B_CONTROL_KEY;
	fShiftDown = modifiers & B_SHIFT_KEY;
	fAltDown = modifiers & B_OPTION_KEY;

	if (fTransformBox) {
		fTransformBox->ModifiersChanged(modifiers);
		return;
	}
	// reevaluate mode
	if (!fMouseDown)
		_SetModeForMousePos(fLastCanvasPos);
}


bool
PathManipulator::HandleKeyDown(uint32 key, uint32 modifiers, Command** _command)
{
	bool result = true;

	float nudgeDist = 1.0;
	if (modifiers & B_SHIFT_KEY)
		nudgeDist /= fCanvasView->ZoomLevel();

	switch (key) {
		// commit
		case B_RETURN:
			if (fTransformBox) {
				_SetTransformBox(NULL);
			}// else
//				_Perform();
			break;
		// cancel
		case B_ESCAPE:
			if (fTransformBox) {
				fTransformBox->Cancel();
				_SetTransformBox(NULL);
			} else if (fFallBackMode == NEW_PATH) {
				fFallBackMode = SELECT_POINTS;
				_SetTransformBox(NULL);
			}// else
//				_Cancel();
			break;
		case 't':
		case 'T':
			if (!fSelection->IsEmpty())
				_SetMode(TRANSFORM_POINTS);
			else
				result = false;
			break;
		// nudging
		case B_UP_ARROW:
			_Nudge(BPoint(0.0, -nudgeDist));
			break;
		case B_DOWN_ARROW:
			_Nudge(BPoint(0.0, nudgeDist));
			break;
		case B_LEFT_ARROW:
			_Nudge(BPoint(-nudgeDist, 0.0));
			break;
		case B_RIGHT_ARROW:
			_Nudge(BPoint(nudgeDist, 0.0));
			break;

		case B_DELETE:
			if (!fSelection->IsEmpty())
				*_command = _Delete();
			else
				result = false;
			break;

		default:
			result = false;
	}
	return result;
}


bool
PathManipulator::HandleKeyUp(uint32 key, uint32 modifiers, Command** _command)
{
	bool handled = true;
	switch (key) {
		// nudging
		case B_UP_ARROW:
		case B_DOWN_ARROW:
		case B_LEFT_ARROW:
		case B_RIGHT_ARROW:
			*_command = _FinishNudging();
			break;
		default:
			handled = false;
			break;
	}
	return handled;
}


bool
PathManipulator::UpdateCursor()
{
	if (fTransformBox)
		return fTransformBox->UpdateCursor();

	const uchar* cursorData;
	switch (fMode) {
		case ADD_POINT:
			cursorData = kPathAddCursor;
			break;
		case INSERT_POINT:
			cursorData = kPathInsertCursor;
			break;
		case MOVE_POINT:
		case MOVE_POINT_IN:
		case MOVE_POINT_OUT:
		case TRANSLATE_POINTS:
			cursorData = kPathMoveCursor;
			break;
		case CLOSE_PATH:
			cursorData = kPathCloseCursor;
			break;
		case TOGGLE_SHARP:
		case TOGGLE_SHARP_IN:
		case TOGGLE_SHARP_OUT:
			cursorData = kPathSharpCursor;
			break;
		case REMOVE_POINT:
		case REMOVE_POINT_IN:
		case REMOVE_POINT_OUT:
			cursorData = kPathRemoveCursor;
			break;
		case SELECT_POINTS:
			cursorData = kPathSelectCursor;
			break;

		case SELECT_SUB_PATH:
			cursorData = B_HAND_CURSOR;
			break;

		case UNDEFINED:
		default:
			cursorData = kStopCursor;
			break;
	}
	BCursor cursor(cursorData);
	fCanvasView->SetViewCursor(&cursor, true);
	fCanvasView->Sync();

	return true;
}


void
PathManipulator::AttachedToView(BView* view)
{
	fCanvasView = dynamic_cast<CanvasView*>(view);
}


void
PathManipulator::DetachedFromView(BView* view)
{
	fCanvasView = NULL;
}


// #pragma mark -


void
PathManipulator::ObjectChanged(const Observable* object)
{
	// TODO: refine VectorPath listener interface and
	// implement more efficiently
	BRect currentBounds = _ControlPointRect();
	_InvalidateCanvas(currentBounds | fPreviousBounds);
	fPreviousBounds = currentBounds;

	// reevaluate mode
	if (!fMouseDown && !fTransformBox)
		_SetModeForMousePos(fLastCanvasPos);
}


// #pragma mark -


void
PathManipulator::PointAdded(int32 index)
{
	ObjectChanged(fPath);
}


void
PathManipulator::PointRemoved(int32 index)
{
	fSelection->Remove(index);
	ObjectChanged(fPath);
}


void
PathManipulator::PointChanged(int32 index)
{
	ObjectChanged(fPath);
}


void
PathManipulator::PathChanged()
{
	ObjectChanged(fPath);
}


void
PathManipulator::PathClosedChanged()
{
	ObjectChanged(fPath);
}


void
PathManipulator::PathReversed()
{
	// reverse selection along with path
	int32 count = fSelection->CountItems();
	int32 pointCount = fPath->CountPoints();
	if (count > 0) {
		Selection temp;
		for (int32 i = 0; i < count; i++) {
			temp.Add((pointCount - 1) - fSelection->IndexAt(i));
		}
		*fSelection = temp;
	}

	ObjectChanged(fPath);
}


// #pragma mark -


uint32
PathManipulator::ControlFlags() const
{
	uint32 flags = 0;

//	flags |= SHAPE_UI_FLAGS_CAN_REVERSE_PATH;
//
//	if (!fSelection->IsEmpty())
//		flags |= SHAPE_UI_FLAGS_HAS_SELECTION;
//	if (fPath->CountPoints() > 1)
//		flags |= SHAPE_UI_FLAGS_CAN_CLOSE_PATH;
//	if (fPath->IsClosed())
//		flags |= SHAPE_UI_FLAGS_PATH_IS_CLOSED;
//	if (fTransformBox)
//		flags |= SHAPE_UI_FLAGS_IS_TRANSFORMING;

	return flags;
}


void
PathManipulator::ReversePath()
{
	int32 count = fSelection->CountItems();
	int32 pointCount = fPath->CountPoints();
	if (count > 0) {
		Selection temp;
		for (int32 i = 0; i < count; i++) {
			temp.Add((pointCount - 1) - fSelection->IndexAt(i));
		}
		*fSelection = temp;
	}
	fPath->Reverse();
}


// #pragma mark -


void
PathManipulator::_SetMode(uint32 mode)
{
	if (fMode != mode) {
//printf("switching mode: %s -> %s\n", string_for_mode(fMode), string_for_mode(mode));
		fMode = mode;

		if (fMode == TRANSFORM_POINTS) {
			_SetTransformBox(new TransformPointsBox(fCanvasView,
													this,
													fPath,
													fSelection->Items(),
													fSelection->CountItems()));
//			fCanvasView->Perform(new EnterTransformPointsCommand(this,
//														  fSelection->Items(),
//														  fSelection->CountItems()));
		} else {
			if (fTransformBox)
				_SetTransformBox(NULL);
		}

		if (BWindow* window = fCanvasView->Window()) {
			window->PostMessage(MSG_UPDATE_SHAPE_UI);
		}
		UpdateCursor();
	}
}


void
PathManipulator::_SetTransformBox(TransformPointsBox* transformBox)
{
	if (fTransformBox == transformBox)
		return;

	BRect dirty(LONG_MAX, LONG_MAX, LONG_MIN, LONG_MIN);
	if (fTransformBox) {
		// get rid of transform box display
		dirty = fTransformBox->Bounds();
		delete fTransformBox;
	}

	fTransformBox = transformBox;

	// TODO: this is weird, fMode should only be set in _SetMode, not
	// here as well, also this method could be called this way
	// _SetModeForMousePos -> _SetMode -> _SetTransformBox
	// and then below it does _SetModeForMousePos again...
	if (fTransformBox) {
		fTransformBox->MouseMoved(fLastCanvasPos);
		if (fMode != TRANSFORM_POINTS) {
			fMode = TRANSFORM_POINTS;
		}
		dirty = dirty | fTransformBox->Bounds();
	} else {
		if (fMode == TRANSFORM_POINTS) {
			_SetModeForMousePos(fLastCanvasPos);
		}
	}

	if (dirty.IsValid()) {
		dirty.InsetBy(-8, -8);
		fCanvasView->Invalidate(dirty);
	}
}


void
PathManipulator::_AddPoint(BPoint where)
{
	if (fPath->AddPoint(where)) {
		fCurrentPathPoint = fPath->CountPoints() - 1;

		delete fAddPointCommand;
		fAddPointCommand = new AddPointCommand(fPath, fCurrentPathPoint,
											   fSelection->Items(),
											   fSelection->CountItems());

		_Select(fCurrentPathPoint, fShiftDown);
	}
}


BPoint
scale_point(BPoint a, BPoint b, float scale)
{
	return BPoint(a.x + (b.x - a.x) * scale,
				  a.y + (b.y - a.y) * scale);
}


void
PathManipulator::_InsertPoint(BPoint where, int32 index)
{
	double scale;

	BPoint point;
	BPoint pointIn;
	BPoint pointOut;

	BPoint previous;
	BPoint previousOut;
	BPoint next;
	BPoint nextIn;

	if (fPath->FindBezierScale(index - 1, where, &scale)
		&& scale >= 0.0 && scale <= 1.0
		&& fPath->GetPoint(index - 1, scale, point)) {

		fPath->GetPointAt(index - 1, previous);
		fPath->GetPointOutAt(index - 1, previousOut);
		fPath->GetPointAt(index, next);
		fPath->GetPointInAt(index, nextIn);

		where = scale_point(previousOut, nextIn, scale);

		previousOut = scale_point(previous, previousOut, scale);
		nextIn = scale_point(next, nextIn, 1 - scale);
		pointIn = scale_point(previousOut, where, scale);
		pointOut = scale_point(nextIn, where, 1 - scale);
		
		if (fPath->AddPoint(point, index)) {

			fPath->SetPointIn(index, pointIn);
			fPath->SetPointOut(index, pointOut);

			delete fInsertPointCommand;
			fInsertPointCommand = new InsertPointCommand(fPath, index,
														 fSelection->Items(),
														 fSelection->CountItems());

			fPath->SetPointOut(index - 1, previousOut);
			fPath->SetPointIn(index + 1, nextIn);

			fCurrentPathPoint = index;
			_ShiftSelection(fCurrentPathPoint, 1);
			_Select(fCurrentPathPoint, fShiftDown);
		}
	}
}


void
PathManipulator::_SetInOutConnected(int32 index, bool connected)
{
	fPath->SetInOutConnected(index, connected);
}


void
PathManipulator::_SetSharp(int32 index)
{
	BPoint p;
	fPath->GetPointAt(index, p);
	fPath->SetPoint(index, p, p, p, true);
}


void
PathManipulator::_RemoveSelection()
{
	// NOTE: copy selection since removing points will
	// trigger notifications, and that will influence the
	// selection
	Selection selection = *fSelection;
	int32 count = selection.CountItems();
	for (int32 i = 0; i < count; i++) {
		if (!fPath->RemovePoint(selection.IndexAt(i) - i))
			break;
	}

	fPath->SetClosed(fPath->IsClosed() && fPath->CountPoints() > 1);

	fSelection->MakeEmpty();
}


void
PathManipulator::_RemovePoint(int32 index)
{
	if (fPath->RemovePoint(index)) {
		_Deselect(index);
		_ShiftSelection(index + 1, -1);
	}
}


void
PathManipulator::_RemovePointIn(int32 index)
{
	BPoint p;
	if (fPath->GetPointAt(index, p)) {
		fPath->SetPointIn(index, p);
		fPath->SetInOutConnected(index, false);
	}
}


void
PathManipulator::_RemovePointOut(int32 index)
{
	BPoint p;
	if (fPath->GetPointAt(index, p)) {
		fPath->SetPointOut(index, p);
		fPath->SetInOutConnected(index, false);
	}
}


Command*
PathManipulator::_Delete()
{
	Command* command = NULL;
	if (!fMouseDown) {
		// make sure we apply an on-going transformation before we proceed
		if (fTransformBox) {
			_SetTransformBox(NULL);
		}

		if (fSelection->CountItems() == fPath->CountPoints()) {
//			command = new RemovePathCommand(fPath);
		} else {
			command = new RemovePointsCommand(fPath,
											  fSelection->Items(),
											  fSelection->CountItems());
			_RemoveSelection();
		}

		_SetModeForMousePos(fLastCanvasPos);
	}

	return command;
}


// #pragma mark -


void
PathManipulator::_Select(BRect r)
{
	BPoint p;
	BPoint pIn;
	BPoint pOut;
	int32 count = fPath->CountPoints();
	Selection temp;
	for (int32 i = 0; i < count && fPath->GetPointsAt(i, p, pIn, pOut); i++) {
		if (r.Contains(p) || r.Contains(pIn) || r.Contains(pOut)) {
			temp.Add(i);
		}
	}
	// merge old and new selection
	count = fOldSelection->CountItems();
	for (int32 i = 0; i < count; i++) {
		int32 index = fOldSelection->IndexAt(i);
		if (temp.Contains(index))
			temp.Remove(index);
		else
			temp.Add(index);
	}
	if (temp != *fSelection) {
		*fSelection = temp;
		_UpdateSelection();
	}
}


void
PathManipulator::_Select(int32 index, bool extend)
{
	if (!extend)
		fSelection->MakeEmpty();
	if (fSelection->Contains(index))
		fSelection->Remove(index);
	else
		fSelection->Add(index);
	// TODO: this can lead to unnecessary invalidation (maybe need to investigate)
	_UpdateSelection();
}


void
PathManipulator::_Select(const int32* indices, int32 count, bool extend)
{
	if (extend) {
		for (int32 i = 0; i < count; i++) {
			if (!fSelection->Contains(indices[i]))
				fSelection->Add(indices[i]);
		}
	} else {
		fSelection->MakeEmpty();
		for (int32 i = 0; i < count; i++) {
			fSelection->Add(indices[i]);
		}
	}
	_UpdateSelection();
}


void
PathManipulator::_Deselect(int32 index)
{
	if (fSelection->Contains(index)) {
		fSelection->Remove(index);
		_UpdateSelection();
	}
}


void
PathManipulator::_ShiftSelection(int32 startIndex, int32 direction)
{
	int32 count = fSelection->CountItems();
	if (count > 0) {
		for (int32 i = 0; i < count; i++) {
			int32 index = fSelection->IndexAt(i);
			if (index >= startIndex) {
				fSelection->Remove(index);
				fSelection->Add(index + direction);
			}
		}
	}
	_UpdateSelection();
}


bool
PathManipulator::_IsSelected(int32 index) const
{
	return fSelection->Contains(index);
}


// #pragma mark -


void
PathManipulator::_InvalidateCanvas(BRect rect) const
{
	// convert from canvas to view space
	fCanvasView->ConvertFromCanvas(&rect);
	fCanvasView->Invalidate(rect);
}


void
PathManipulator::_InvalidateHighlightPoints(int32 newIndex, uint32 newMode)
{
	BRect oldRect = _ControlPointRect(fCurrentPathPoint, fMode);
	BRect newRect = _ControlPointRect(newIndex, newMode);
	if (oldRect.IsValid())
		_InvalidateCanvas(oldRect);
	if (newRect.IsValid())
		_InvalidateCanvas(newRect);
}


void
PathManipulator::_UpdateSelection() const
{
	_InvalidateCanvas(_ControlPointRect());
	if (BWindow* window = fCanvasView->Window()) {
		window->PostMessage(MSG_UPDATE_SHAPE_UI);
	}
}


BRect
PathManipulator::_ControlPointRect() const
{
	BRect r = fPath->ControlPointBounds();
	r.InsetBy(-POINT_EXTEND, -POINT_EXTEND);
	return r; 
}


BRect
PathManipulator::_ControlPointRect(int32 index, uint32 mode) const
{
	BRect rect(0.0, 0.0, -1.0, -1.0);
	if (index >= 0) {
		BPoint p, pIn, pOut;
		fPath->GetPointsAt(index, p, pIn, pOut);
		switch (mode) {
			case MOVE_POINT:
			case TOGGLE_SHARP:
			case REMOVE_POINT:
			case CLOSE_PATH:
				rect.Set(p.x, p.y, p.x, p.y);
				rect.InsetBy(-POINT_EXTEND, -POINT_EXTEND);
				break;
			case MOVE_POINT_IN:
			case TOGGLE_SHARP_IN:
			case REMOVE_POINT_IN:
				rect.Set(pIn.x, pIn.y, pIn.x, pIn.y);
				rect.InsetBy(-CONTROL_POINT_EXTEND, -CONTROL_POINT_EXTEND);
				break;
			case MOVE_POINT_OUT:
			case TOGGLE_SHARP_OUT:
			case REMOVE_POINT_OUT:
				rect.Set(pOut.x, pOut.y, pOut.x, pOut.y);
				rect.InsetBy(-CONTROL_POINT_EXTEND, -CONTROL_POINT_EXTEND);
				break;
			case SELECT_POINTS:
				rect.Set(min4(p.x, pIn.x, pOut.x, pOut.x),
						 min4(p.y, pIn.y, pOut.y, pOut.y),
						 max4(p.x, pIn.x, pOut.x, pOut.x),
						 max4(p.y, pIn.y, pOut.y, pOut.y));
				rect.InsetBy(-POINT_EXTEND, -POINT_EXTEND);
				break;
		}
	}
	return rect;
}


// #pragma mark -


void
PathManipulator::_SetModeForMousePos(BPoint where)
{
	uint32 mode = UNDEFINED;
	int32 index = -1;

	float zoomLevel = fCanvasView->ZoomLevel();

	// see if we're close enough at a control point
	BPoint point;
	BPoint pointIn;
	BPoint pointOut;
	for (int32 i = 0; fPath->GetPointsAt(i, point, pointIn, pointOut)
					  && mode == UNDEFINED; i++) {

		float distM = point_point_distance(point, where) * zoomLevel;
		float distIn = point_point_distance(pointIn, where) * zoomLevel;
		float distOut = point_point_distance(pointOut, where) * zoomLevel;
		
		if (distM < MOVE_THRESHOLD) {
			if (i == 0 && fClickToClose
				&& !fPath->IsClosed() && fPath->CountPoints() > 1) {
				mode = fCommandDown ? TOGGLE_SHARP :
							(fOptionDown ? REMOVE_POINT : CLOSE_PATH);
				index = i;
			} else {
				mode = fCommandDown ? TOGGLE_SHARP :
							(fOptionDown ? REMOVE_POINT : MOVE_POINT);
				index = i;
			}
		}
		if (distM - distIn > 0.00001
			&& distIn < MOVE_THRESHOLD) {
			mode = fCommandDown ? TOGGLE_SHARP_IN : 
						(fOptionDown ? REMOVE_POINT_IN : MOVE_POINT_IN);
			index = i;
		}
		if (distIn - distOut > 0.00001
			&& distOut < distM && distOut < MOVE_THRESHOLD) {
			mode = fCommandDown ? TOGGLE_SHARP_OUT :
						(fOptionDown ? REMOVE_POINT_OUT : MOVE_POINT_OUT);
			index = i;
		}
	}
	// selection mode overrides any other mode,
	// but we need to check for it after we know
	// the index of the point under the mouse (code above)
	int32 pointCount = fPath->CountPoints();
	if (fShiftDown && pointCount > 0) {
		mode = SELECT_POINTS;
	}

	// see if user wants to start new sub path
	if (fAltDown) {
		mode = NEW_PATH;
		index = -1;
	}

	// see if we're close enough at a line
	if (mode == UNDEFINED) {
		float distance;
		if (fPath->GetDistance(where, &distance, &index)) {
			if (distance < (INSERT_DIST_THRESHOLD / zoomLevel)) {
				mode = INSERT_POINT;
			}
		} else {
			// restore index, since it was changed by call above
			index = fCurrentPathPoint;
		}
	}

	// nope, still undefined mode, last fall back
	if (mode == UNDEFINED) {
		if (fFallBackMode == SELECT_POINTS) {
			if (fPath->IsClosed() && pointCount > 0) {
				mode = SELECT_POINTS;
				index = -1;
			} else {
				mode = ADD_POINT;
				index = pointCount - 1;
			}
		} else {
			// user had clicked "New Path" icon
			mode = fFallBackMode;
		}
	}
	// switch mode if necessary
	if (mode != fMode || index != fCurrentPathPoint) {
		// invalidate path display (to highlight the respective point)
		_InvalidateHighlightPoints(index, mode);
		_SetMode(mode);
		fCurrentPathPoint = index;
	}
}


// #pragma mark -


void
PathManipulator::_Nudge(BPoint direction)
{
	bigtime_t now = system_time();
	if (now - fLastNudgeTime > 500000) {
		fCanvasView->Perform(_FinishNudging());
	}
	fLastNudgeTime = now;
	fNudgeOffset += direction;

	if (fTransformBox) {
		fTransformBox->NudgeBy(direction);
		return;
	}

	if (!fNudgeCommand) {

		bool fromSelection = !fSelection->IsEmpty();

		int32 count = fromSelection ? fSelection->CountItems()
									: fPath->CountPoints();
		int32 indices[count];
		BStackOrHeapArray<control_point, 64> points(count);

		// init indices and points
		for (int32 i = 0; i < count; i++) {
			indices[i] = fromSelection ? fSelection->IndexAt(i) : i;
			fPath->GetPointsAt(indices[i],
							   points[i].point,
							   points[i].point_in,
							   points[i].point_out,
							   &points[i].connected);
		}

		fNudgeCommand = new NudgePointsCommand(fPath, indices, points, count);

		fNudgeCommand->SetNewTranslation(fNudgeOffset);
		fNudgeCommand->Redo();

	} else {
		fNudgeCommand->SetNewTranslation(fNudgeOffset);
		fNudgeCommand->Redo();
	}

	if (!fMouseDown)
		_SetModeForMousePos(fLastCanvasPos);
}


Command*
PathManipulator::_FinishNudging()
{
	fNudgeOffset = BPoint(0.0, 0.0);

	Command* command;

	if (fTransformBox) {
		command = fTransformBox->FinishNudging();
	} else {
		command = fNudgeCommand;
		fNudgeCommand = NULL;
	}

	return command;
}
